﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace PalmAddressExtractor
{
	public class AddressReader
	{
		#region Enums

		private enum FieldType : uint
		{
			None,
			Integer,
			Float,
			Date,
			Alpha,
			String,
			Boolean,
			Bitflag,
			RepeatEvent
		}

		#endregion

		#region Properties

		private Stream Stream { get; set; }

		#endregion

		#region Constructors

		public AddressReader(Stream stream)
		{
			Stream = stream;
		}

		#endregion

		#region Address read methods

		public AddressBook ReadAddressBook()
		{
			var book = new AddressBook();

			var header = ReadHeader();

			book.Categories = ReadCategories(header.CategoryCount);

			var schemaInfo = ReadSchemaInfo();

			book.Addresses = ReadAddresses(schemaInfo, book.Categories);

			return book;
		}

		private AddressHeader ReadHeader()
		{
			var header = new AddressHeader();

			header.Version = ReadUInt32();

			header.Filename = ReadString();
			header.StringTable = ReadString();

			header.NextCategoryId = ReadUInt32();
			header.CategoryCount = ReadUInt32();

			return header;
		}

		private Dictionary<uint, Category> ReadCategories(uint categoryCount)
		{
			var categories = new Dictionary<uint, Category>();

			for (var categoryLoop = 0u; categoryLoop < categoryCount; categoryLoop++)
			{
				var category = new Category
				{
					Index = ReadUInt32(),
					Id = ReadUInt32(),
					DirtyFlag = ReadUInt32(),
					LongName = ReadString(),
					ShortName = ReadString()
				};

				categories.Add(category.Id, category);
			}

			return categories;
		}

		private SchemaInfo ReadSchemaInfo()
		{
			var schemaInfo = new SchemaInfo();

			schemaInfo.ResourceId = ReadUInt32();
			schemaInfo.FieldsPerRow = ReadUInt32();
			schemaInfo.RecordIdPosition = ReadUInt32();
			schemaInfo.RecordStatusPosition = ReadUInt32();
			schemaInfo.PlacementPosition = ReadUInt32();
			schemaInfo.FieldCount = ReadUInt16();

			for (var fieldLoop = 0u; fieldLoop < schemaInfo.FieldCount; fieldLoop++)
				schemaInfo.FieldEntries.Add(ReadUInt16());

			schemaInfo.RecordCount = ReadUInt32() / schemaInfo.FieldsPerRow;

			return schemaInfo;
		}

		private IList<Address> ReadAddresses(SchemaInfo schemaInfo, IDictionary<uint, Category> categories)
		{
			var addresses = new List<Address>();

			for (var addressLoop = 0u; addressLoop < schemaInfo.RecordCount; addressLoop++)
			{
				var address = new Address();
				var record = ReadRecord(schemaInfo);

				address.LastName = record[3];
				address.FirstName = record[4];
				address.Title = record[5];
				address.CompanyName = record[6];

				for (var phoneLoop = 0; phoneLoop < 5; phoneLoop++)
					address.PhoneNumbers.Add(record[8 + (phoneLoop * 2)]);

				address.Street = record[17];
				address.City = record[18];
				address.State = record[19];
				address.Zip = record[20];
				address.Country = record[21];
				address.Note = record[22];
				address.Category = categories.ContainsKey(record[24]) ? categories[record[24]] : null;

				for (var customLoop = 0; customLoop < 4; customLoop++)
					address.CustomData.Add(record[25 + customLoop]);

				addresses.Add(address);
			}

			return addresses;
		}

		private List<dynamic> ReadRecord(SchemaInfo schemaInfo)
		{
			var fields = new List<dynamic>();

			for (var fieldLoop = 0u; fieldLoop < schemaInfo.FieldsPerRow; fieldLoop++)
			{
				var fieldTypeValue = ReadUInt32();
				var fieldType = (FieldType)fieldTypeValue;

				switch (fieldType)
				{
					case FieldType.Integer:
					case FieldType.Boolean:
					case FieldType.Bitflag:
						fields.Add(ReadUInt32());
						break;
					case FieldType.String:
						ReadInt32(); //Discard the padding value (perhaps we should check to make sure it is always zero?)
						fields.Add(ReadString().Replace("\r", string.Empty).Replace("\n", string.Empty).Replace(",", " "));
						break;
					default:
						throw new Exception(string.Format("Unknown type: {0}", fieldTypeValue));
				}
			}

			return fields;
		}

		#endregion

		#region Primitive read methods

		private int ReadByte()
		{
			return Stream.ReadByte();
		}

		private uint ReadUInt8()
		{
			return (uint)Stream.ReadByte();
		}

		private int ReadInt16()
		{
			var buffer = new byte[sizeof(Int16)];
			Stream.Read(buffer, 0, buffer.Length);

			return BitConverter.ToInt16(buffer, 0);
		}

		private uint ReadUInt16()
		{
			var buffer = new byte[sizeof(UInt16)];
			Stream.Read(buffer, 0, buffer.Length);

			return BitConverter.ToUInt16(buffer, 0);
		}

		private int ReadInt32()
		{
			var buffer = new byte[sizeof(Int32)];
			Stream.Read(buffer, 0, buffer.Length);

			return BitConverter.ToInt32(buffer, 0);
		}

		private uint ReadUInt32()
		{
			var buffer = new byte[sizeof(UInt32)];
			Stream.Read(buffer, 0, buffer.Length);

			return BitConverter.ToUInt32(buffer, 0);
		}

		private string ReadString()
		{
			var length = ReadUInt8();

			if (length == 0)
				return string.Empty;
			else if (length == 255)
				length = ReadUInt16();

			var buffer = new byte[length];
			Stream.Read(buffer, 0, buffer.Length);

			return Encoding.ASCII.GetString(buffer);
		}

		#endregion
	}
}
